Skip to content

fix(spa): announce the UsersPage load-failure to screen readers - #419

Merged
mforce merged 2 commits into
mainfrom
fix/userspage-load-error-alert
Aug 4, 2026
Merged

mforce merged 2 commits into
mainfrom
fix/userspage-load-error-alert

Conversation

@mforce

@mforce mforce commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Closes #389.

UsersPage rendered its load-failure (error && users === null) as a bare <p className="error">, while every sibling screen — AuditPage, ReportsPage, ExportPage, StockPage, HistoryPage, SettingsPage — uses role="alert" for the same case. role="alert" is an implicit live region, so without it a screen-reader user who deep-links to /users and is refused hears the heading and then silence, while the visual user sees red text. This adds role="alert" to match the siblings.

Diff

-  if (error && users === null) return <section><h2>{t("heading")}</h2><p className="error">{error}</p></section>;
+  if (error && users === null) return <section><h2>{t("heading")}</h2><p className="error" role="alert">{error}</p></section>;

Scope

The unambiguous load-failure only. The dialog-level and page-level error paragraphs (UsersPage.tsx ~399, 408–409, 484, 501, 541) are left as-is: per #389 those are a judgment call — a dialog error adjacent to its field may be better served by aria-describedby than by an alert — and belong in a separate pass.

Testing

🤖 Generated with Claude Code

https://claude.ai/code/session_01LSkqgsC9Sj4arUdevUSJfJ


Generated by Claude Code

UsersPage rendered its load-failure (error && users === null) as a bare
`<p className="error">`, while every sibling screen — AuditPage, ReportsPage,
ExportPage, StockPage, HistoryPage, SettingsPage — uses role="alert" for the
same case. role="alert" is an implicit live region, so without it a
screen-reader user who deep-links to /users and is refused hears the heading
and then silence, while the visual user sees red text. Add role="alert" to
match the siblings.

Scope is the unambiguous load-failure only. The dialog-level and page-level
error paragraphs are left as-is: per #389 those are a judgment call (a
dialog error adjacent to its field may be better served by aria-describedby
than by an alert), and belong in a separate pass.

No unit test added: UsersPage.test.tsx documents (its "UsersPage load" block)
that the mount-effect rejection branch cannot be asserted in this Vitest 3 +
React 19 stack without a false-positive unhandled rejection. Per #389 the
#385 Playwright ReadOnly deep-link spec is where this tightens to
getByRole("alert"). typecheck clean; the 58 existing UsersPage tests pass.

Closes #389.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSkqgsC9Sj4arUdevUSJfJ
@mforce

mforce commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@codex please review.

One-line a11y fix: role="alert" on the UsersPage load-failure paragraph, matching the sibling screens. Worth checking adversarially:

  • Right element, right case. Confirm UsersPage.tsx:347 is the load-failure branch (error && users === null) and that role="alert" there matches how AuditPage/ReportsPage/StockPage/HistoryPage/SettingsPage mark the same state — not a divergent pattern.
  • Scope. The dialog/page-level error paragraphs are intentionally untouched (per a11y: UsersPage refuses a load with no role=alert, so the refusal is silent to screen readers #389 they're a per-field judgment call). Flag if any of those is actually the same unambiguous load-failure class and should have moved too.
  • No test regression. No unit test was added because UsersPage.test.tsx documents the mount-effect rejection branch as unassertable in this Vitest 3 + React 19 stack. Push back if you think it's testable without the documented false positive.

Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 3b64846002

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Review-loop follow-up. The page-level error banner (shown only when no dialog
is open: error && !creating && openUser === null && editUser === null &&
pwUser === null) was still a bare `<p className="error">`, while the sibling
page-level banners announce — ProductsPage (error && !dialogOpen) and
ExpensesPage both mark theirs role="alert". It is the same unambiguous
page-level class as the load-failure at line 347, not a dialog-adjacent
judgment call, so it gets role="alert" too.

Still deliberately left bare: the dialog-scoped validation errors (create /
assign / edit / password dialogs), which #389 flags as a per-field judgment
call (aria-describedby may beat an alert). typecheck clean; 58 UsersPage tests
pass.

Refs #389.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSkqgsC9Sj4arUdevUSJfJ
@mforce

mforce commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Ran an adversarial review loop (codex + three cheap-model reviewers across two rounds). Outcome:

One real finding, fixed in f40269e: the page-level error banner (UsersPage.tsx:408–409, rendered only when no dialog is open) was still a bare <p className="error">, while the sibling page-level banners announce (ProductsPage error && !dialogOpen, ExpensesPage). It's the same unambiguous non-dialog class as the load-failure at line 347, so it now carries role="alert" too.

Still deliberately deferred: the four dialog-scoped validation errors (create / assign / edit / password dialogs, ~399/484/501/541). Per #389 those are a per-field judgment call (aria-describedby may beat an alert), so they're out of scope for this PR.

Verified: the two alerts are mutually exclusive (line 347 is an early return, so the 408–409 banner only renders once users !== null) — no double announcement. role="alert" mounts after the loading state, so it announces. typecheck clean; 58 UsersPage.test.tsx tests pass; Web CI green.


Generated by Claude Code

@mforce
mforce merged commit 0e4d6bf into main Aug 4, 2026
9 checks passed
@mforce
mforce deleted the fix/userspage-load-error-alert branch August 4, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a11y: UsersPage refuses a load with no role=alert, so the refusal is silent to screen readers

2 participants